5.02. Типы данных
Типы данных
Python — язык, в котором всё является объектом. Это означает, что каждое значение имеет свой тип, и каждый тип определяет, какие операции можно с ним выполнять.
Типизация данных — это набор правил, по которым язык программирования определяет, к какому типу принадлежит значение, и какие действия с ним можно совершать. Можно ли складывать два значения, можно ли вызывать метод, и что произойдёт если выполнить какую-то конкретную операцию. Ответы на эти вопросы зависят от системы типизации языка.
Python часто описывают тремя эпитетами: неявная, динамическая и сильная типизация.
Динамическая типизация подразумевает, что тип переменной определяется во время выполнения программы, а не при объявлении.
x = 42 # x — int
x = "привет" # теперь x — str
x = [1, 2, 3] # и вот уже x — list
Переменная x может менять свой тип в любой момент. Никаких аннотаций не требуется. Это противоположно статической типизации (например, в Java или C++), где тип переменной указывается явно и не меняется.
Неявная типизация говорит о том, что Python автоматически определяет тип значения без необходимости писать его явно.
age = 30 # Python сам понимает: это int
name = "Алиса" # Это str
Мы не пишем int age = 30; — интерпретатор сам «догадывается». Это не то же самое, что «слабая типизация». Неявность — про удобство записи, а не про преобразования.
Сильная типизация говорит, что Python не делает автоматических неявных преобразований между несовместимыми типами.
"возраст: " + 25 # ОШИБКА!
Вызовет TypeError: нельзя сложить строку и число. В языках со слабой типизацией (например, JavaScript) такое выражение превратилось бы в "возраст: 25" автоматически.
В Python мы обязаны явно преобразовать:
"возраст: " + str(25) # Теперь всё ок
Сильная типизация помогает избегать логических ошибок, но требует внимательности при работе с разными типами.
Все типы данных в Python можно разделить на две большие категории:
- Изменяемые (mutable, мутабельные) - объект можно изменить после создания (как раз list, dict, set);
- Неизменяемые (immutable, иммутабельные) - значение фиксируется при создании (int, str, tuple, frozenset).
Неизменяемые объекты безопасны при передаче в функции — их нельзя случайно изменить. Они могут использоваться как ключи в словарях (потому что хэшируемы). При «изменении» неизменяемого объекта создаётся новый объект:
s = "hello"
s = s + " world" # Это НОВАЯ строка, старая удаляется сборщиком мусора
Изменяемые объекты позволяют добавлять, удалять или изменять элементы «на месте»:
my_list = [1, 2]
my_list.append(3) # my_list изменился, id остаётся тем же
Python — динамически и сильно типизированный язык. Тип переменной определяется автоматически, но не происходит неявных преобразований между несовместимыми типами (например, int + str вызовет ошибку).
Python использует управляемую ссылками модель памяти: всё в Python — объекты, и переменные являются ссылками на эти объекты, а не контейнерами для значений. Атомарными называются типы данных, которые не могут быть изменены после создания. К ним относятся числа (int, float, complex), строки (str), кортежи (tuple), булевы значения (bool) и None. При «изменении» такого объекта создаётся новый объект, переменная перенаправляется на новое значение, а оригинальный объект остаётся неизменным.
a = 42
b = a # b ссылается на тот же объект
a = a + 1 # Создаётся новый int(43), a указывает на него
print(b) # 42 — значение b не изменилось
Доступна проверка идентичности через id():
x = "hello"
y = "hello"
print(x is y) # Может быть True (интернирование строк)
print(id(x), id(y)) # Часто совпадают для коротких строк
Ссылочные (изменяемые) объекты могут изменять своё содержимое без изменения своей идентичности. К ним относятся:
- Списки (list)
- Словари (dict)
- Множества (set)
- Экземпляры пользовательских классов
Пример:
lst1 = [1, 2, 3]
lst2 = lst1 # lst2 ссылается на тот же объект
lst2.append(4)
print(lst1) # [1, 2, 3, 4] — изменился и lst1!
Чтобы избежать неожиданного поведения, нужно создавать копии:
lst2 = lst1.copy() # Поверхностная копия
# или
lst2 = lst1[:] # Для списков
# или
import copy
lst2 = copy.deepcopy(lst1) # Глубокая копия (для вложенных структур)
Даже атомарные объекты хранятся как объекты в куче. Разница в том, что при «изменении» числа вы получаете новый объект, а при изменении списка — модифицируете существующий.
А теперь рассмотрим все встроенные типы подробнее.
Число
Число (int, float, complex) поддерживает целые (int), вещественные (float) и комплексные числа (complex). int не ограничен по размеру (в отличие от многих других языков). Простой пример:
age = 30
Сложный пример:
import math
def calculate_compound_interest(principal, rate, times_per_year, years):
return principal * (1 + rate / times_per_year) ** (times_per_year * years)
final_amount = round(calculate_compound_interest(1000, 0.05, 12, 10), 2)
Выше мы видим результат функции, вычисляющей сложные проценты, округляется до двух знаков. Переменная final_amount содержит число, полученное через математическую формулу и округление — типичный сценарий финансовых вычислений.
int — целые числа. Могут быть любого размера (ограничены только памятью). Поддерживают отрицательные значения.
x = 100
big_number = 10**100 # Очень большое число — работает!
float — вещественные числа. Представлены в формате с плавающей точкой (IEEE 754). Имеют ограниченную точность (~15–17 знаков).
pi = 3.1415926535
scientific = 6.02e23 # 6.02 × 10²³
Из-за особенностей представления 0.1 + 0.2 != 0.3 — классическая проблема всех float. Для финансовых вычислений лучше использовать модуль decimal.
complex — комплексные числа. Формат: a + bj, где j — мнимая единица.
z = 3 + 4j
print(z.real) # 3.0
print(z.imag) # 4.0
Используются в научных расчётах, сигнал-обработке и математике.
Булево
Булево (bool) это подтип int, где True == 1, False == 0. Возникает как результат сравнений, логических выражений.
Простой пример:
is_active = True
Сложный пример:
is_eligible = (
user['age'] >= 18 and
user['verified'] and
any(role in user['roles'] for role in ['admin', 'moderator']) and
not user.get('blocked', False)
)
Здесь условие включает проверку возраста, флага верификации, наличие одной из ролей через генератор выражения (any) и безопасное извлечение поля с помощью get. Результат — булево значение, вычисленное на основе структурированных данных.
Два значения: True и False. Является подклассом int: True == 1, False == 0.
is_ready = True
if is_ready:
print("Готов!")
Любое значение можно привести к булеву через bool():
bool(0) # False
bool("") # False
bool([1, 2]) # True
Полезно в условиях и проверках.
Строка
Строка (str) это неизменяемая последовательность символов Unicode. Поддерживает f-строки, форматирование, методы, индексацию, срезы.
Строки неизменяемы - любое «изменение» создаёт новую строку.
Простой пример:
name = "Тимур"
Сложный пример:
from datetime import datetime
greeting = f"""
Добро пожаловать, {user.get('name', 'Гость').title()}!
Сегодня: {datetime.now().strftime('%d.%m.%Y')}
Время: {datetime.now().strftime('%H:%M')}
Ваш баланс: {user.get('balance', 0):,.2f} ₽.
""".strip()
Здесь F-строка с вложенными вызовами методов, форматированием даты, заглавными буквами имени и денежного формата (:,.2f). Использовано безопасное извлечение значений и удаление лишних пробелов.
F-строки (форматированные строковые литералы) появились в Python 3.6 — самый удобный способ форматирования строк.
name = "Мария"
age = 28
greeting = f"Привет, {name}! Тебе {age} лет."
В фигурные скобки можно вставлять переменные, вызовы функций, выражения.
result = f"Квадрат 5: {5 ** 2}, длина имени: {len(name)}"
F-строки поддерживают форматирование:
price = 1234.5678
formatted = f"Цена: {price:,.2f} ₽" # "Цена: 1,234.57 ₽"
date = f"Дата: {datetime.now():%d.%m.%Y}"
None
None — специальное значение, обозначающее отсутствие значения.
result = None
У него есть собственный тип: NoneType, и None — единственный экземпляр типа NoneType. В системе типов Python — аналог null (JavaScript), nil (Ruby), NULL (SQL).
Используется как значение по умолчанию, возврат из функций, метка отсутствия данных.
def find_user(user_id):
if user_id in database:
return database[user_id]
return None # пользователя нет
None не то же самое, что False, 0 или пустая строка. При проверке в if ведёт себя как False, но не равен ему:
bool(None) # False
None == False # False!
Правильно проверять: if value is None:
Последовательности (Sequences).
Последовательности — упорядоченные коллекции, доступные по индексу.
Это str, list, tuple.
Кортежи (tuple) представляют собой неизменяемые списки и часто используются для группировки данных. Пусть вас не пугает это слово, оно означает «несколько значений»:
point = (10, 20)
rgb = (255, 128, 0)
Они могут использоваться как ключи в словарях, но быстрее и легче, чем списки.
Словарь
Словарь (dict) это упорядоченная (начиная с Python 3.7) коллекция пар «ключ-значение». Аналог Object в JavaScript или HashMap в Java.
Ключи должны быть хэшируемыми (обычно неизменяемыми). Эффективны для поиска по ключу (O(1)).
Простой пример:
person = {"name": "Иван", "age": 28}
Сложный пример:
config = {
key: value.upper() if isinstance(value, str) and key.endswith('_KEY') else value
for key, value in os.environ.items()
if key.startswith('APP_')
}
Это генератор словаря, который фильтрует переменные окружения по префиксу APP_, и если ключ заканчивается на _KEY, преобразует строковое значение в верхний регистр. Демонстрирует мощь компактного синтаксиса и условной логики.
Множества
Множества (set и frozenset) хранят уникальные элементы без повторений. Неупорядоченные (до Python 3.7), сейчас порядок зависит от версии.
unique_tags = {"python", "web", "backend"}
set — изменяемое множество.
frozenset — неизменяемое, можно использовать как ключ в словаре.
Их используют для удаления дубликатов, операций типа объединения, пересечения, разности. Пример:
a = {1, 2, 3}
b = {3, 4, 5}
a & b # {3} — пересечение
a | b # {1, 2, 3, 4, 5} — объединение
Список
Список (list) это изменяемая упорядоченная коллекция. Аналог массива в JS.
Простой пример:
numbers = [1, 2, 3]
Сложный пример:
filtered_logs = [
{**log, 'timestamp': format_timestamp(log['ts']), 'level_name': LOG_LEVELS[log['level']]}
for log in logs
if log['level'] >= MIN_LEVEL and is_valid_source(log['source'])
]
Это списковое включение с распаковкой словаря (**log), добавлением новых полей, преобразованием уровня логирования и фильтрацией. Результат — новый список с расширенной информацией.
Функция
Функция (function) — объект первого класса. Поддерживают декораторы, замыкания, лямбды.
Простой пример:
def greet(name):
return f"Привет, {name}!"
Сложный пример:
def retry_on_failure(max_attempts=3, delay=1):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise e
time.sleep(delay)
return None
return wrapper
return decorator
@retry_on_failure(max_attempts=5, delay=2)
def fetch_data():
return requests.get("https://api.example.com/data").json()
Здесь приведён в пример декоратор высшего порядка, реализующий повторные попытки выполнения функции при ошибках. Позволяет добавить устойчивость к временным сбоям сети. Широко используется в системах, взаимодействующих с внешними API.
Но функции мы изучим отдельно.
Бинарные данные
Бинарные данные нужны для работы с сырыми байтами (например, файлы, сеть, шифрование).
bytes — неизменяемая последовательность байтов
data = b"Hello"
print(data[0]) # 72 (ASCII-код 'H')
bytearray — изменяемая версия
b = bytearray(b"Hello")
b[0] = 104 # заменяем 'H' на 'h'
print(b) # bytearray(b'hello')
Их используют при чтении/записи файлов в бинарном режиме (open(..., 'rb')), работе с сетевыми протоколами, в криптографии и сериализации.
Например, чтобы перевести строку в байты: text.encode('utf-8'), а чтобы перевести байты в строку: data.decode('utf-8').